/*
 * IM - Image I/O Library
 * Copyright (c) 1996 CENPES/Petrobras, TeCGraf/PUC-Rio.
 * Antonio E. Scuri - scuri@tecgraf.puc-rio.br
 * 
 * IMPCX.C
 * 
 * Contains the implementation of the PCX file format functions.
 *
 * History:
 *          1996.04.05 - Changed the structure of the module to the new format.
 *          1996.06.14 - Finished inicial version.
 *
 */

#include "im.h"
#include "imio.h"
#include "impcx.h"
#include "binfile.h"


#include <stdlib.h>
#include <memory.h>


#define PCX_ID 0x0A


/* PCX file header */
/*  1   Id;             Manufacturer ID        */
/*  1   Version;        Version                */
/*  1   Encoding;       Encoding Scheme        */
/*  1   BitsPerPixel;   Bits/Pixel/Plane       */
/*  2   Xmin;           X Start (upper left)   */
/*  2   Ymin;           Y Start (top)          */
/*  2   Xmax;           X End (lower right)    */
/*  2   Ymax;           Y End (bottom)         */
/*  2   Hdpi;           Horizontal Resolution  */
/*  2   Vdpi;           Vertical Resolution    */
/*  3*16 Colormap;      16-Color EGA Palette   */
/*  1   Reserved;       Reserved               */
/*  1   NPlanes;        Number of Color Planes */
/*  2   BytesPerLine;   Bytes/Line/Plane       */
/*  2   PaletteInfo;    Palette Interpretation */
/*  2   HScreenSize;    Horizontal Screen Size */
/*  2   VScreenSize;    Vertical Screen Size   */
/*  54  Filler;         Reserved               */
/*  128    */


/* Default 16 color VGA palette */
static unsigned char imPCXDefaultPalette[3*16] = 
{
    0,   0,   0,
    0,   0, 255,
    0, 255,   0,
    0, 255, 255,
  255,   0,   0,
  255,   0, 255,
  255, 255,   0,
  255, 255, 255,
   85,  85, 255,
   85,  85,  85,
    0, 170,   0,
  170,   0,   0,
   85, 255, 255,
  255,  85, 255,
  255, 255,  85,
  255, 255, 255  
};



typedef struct _imFilePCX
{
  binFile* file_bin;           /* the binary file handle */
  int bpp;                     /* number of bits per pixel */
  unsigned char version;       /* format version */
  unsigned char encoding;      /* PCX compression information */
  unsigned short bplp;         /* bytes per line per plane */
  unsigned long line_size;     /* line buffer size */
  unsigned char* line_buffer;  /* line buffer */
  unsigned char* rgb_line_buffer;  /* rgb compact line buffer */
} imFilePCX;


static int imPCXEncodeScanLine(unsigned char* EncodedBuffer, unsigned char* DecodedBuffer, int BufferSize)
{
  int index = 0;        /* Index into uncompressed data buffer  */
  int scanindex = 0;    /* Index into compressed data buffer    */
  unsigned char runcount;                  /* Length of encoded pixel run          */
  unsigned char runvalue;                  /* Value of encoded pixel run           */

  while (index < BufferSize)
  {
    /** Get the run count of the next pixel value run.
    ** Pixel value runs are encoded until a different pixel value
    ** is encountered, the end of the scan line is reached, or 63
    ** pixel values have been counted. */
    for (runcount = 1, runvalue = DecodedBuffer[index]; runvalue == DecodedBuffer[index + runcount] && index + runcount < BufferSize && runcount < 63; runcount++);

    /** Encode the run into a one or two-unsigned char code.
    ** Multiple pixel runs are stored in two-unsigned char codes.  If a single
    ** pixel run has a value of less than 64 then it is stored in a
    ** one-unsigned char code.  If a single pixel run has a value of 64 to 255
    ** then it is stored in a two-unsigned char code. */

    if (runcount > 1)                   /* Multiple pixel run */
    {
      EncodedBuffer[scanindex++] = (unsigned char)(runcount | 0xC0);
      EncodedBuffer[scanindex++] = runvalue;
    }
    else                                /* Single pixel run   */
    {
      if (DecodedBuffer[index] < 64)  /* Value is 0 to 63   */
        EncodedBuffer[scanindex++] = runvalue;
      else                            /* Value is 64 to 255 */
      {
        EncodedBuffer[scanindex++] = (unsigned char)(runcount | 0xC0);
        EncodedBuffer[scanindex++] = runvalue;
      }
    }

    index += runcount;  /* Jump ahead to next pixel run value */
  }

  return(scanindex);      /* Return the number of unsigned chars written to buffer */
}


static int imPCXDecodeScanLine(binFile* file_bin, unsigned char* DecodedBuffer, int BufferSize)
{
  int  index = 0;    /* Index into compressed scan line buffer */
  unsigned char data;          /* Data byte read from PCX file           */
  unsigned char runcount = 0;   /* Length of decoded pixel run            */
  unsigned char runvalue = 0;   /* Value of decoded pixel run             */

  while (index < BufferSize)    /* Read until the end of the buffer     */
  {
    binFileRead1ByteBuf(file_bin, &data, 1);

    if ((data & 0xC0) == 0xC0)              /* Two-unsigned char code    */
    {
      runcount = (unsigned char)(data & 0x3F);             /* Get run count    */
      binFileRead1ByteBuf(file_bin, &runvalue, 1);
    }
    else                                    /* One unsigned char code    */
    {
      runcount = 1;                       /* Run count is one */
      runvalue = data;                    /* Pixel value      */
    }

    if (binFileError(file_bin))
      return IM_ERR_READ;

    /* Write the pixel run to the buffer */
    for (;runcount && (index < BufferSize); runcount--, index++)
      DecodedBuffer[index] = runvalue;    /* Assign value to buffer   */
  }

  return IM_ERR_NONE;      
}


static imFile* imPCXReadOpen(char* filename, int *err)
{
  unsigned char id, bppp, planes;
  unsigned short xmin, xmax, ymax, ymin, word;
  imFile *ifile = (imFile*)malloc(sizeof(imFile));
  imFilePCX *file_pcx = (imFilePCX*)malloc(sizeof(imFilePCX));
  binFile* file_bin;

  ifile->file = file_pcx;

  /* opens the binary file for reading with intel unsigned char order */
  file_bin = binFileOpen(filename, 0, 0);
  if (file_bin == NULL)
  {
    free(file_pcx);
    free(ifile);
    if (err) *err = IM_ERR_OPEN;
    return NULL;
  }

  file_pcx->file_bin = file_bin;

  /* reads the PCX format identifier */
  binFileRead1ByteBuf(file_bin, &id, 1);
  if (binFileError(file_bin))
  {
    binFileClose(file_bin);
    free(file_pcx);
    free(ifile);
    if (err) *err = IM_ERR_READ;
    return NULL;
  }

  if (id != PCX_ID)
  {
    binFileClose(file_bin);
    free(file_pcx);
    free(ifile);
    if (err) *err = IM_ERR_FORMAT;
    return NULL;
  }

  /* reads the format version */
  binFileRead1ByteBuf(file_bin, &file_pcx->version, 1);

  /* reads the compression encoding */
  binFileRead1ByteBuf(file_bin, &file_pcx->encoding, 1);
  ifile->compress = file_pcx->encoding? IM_DEFAULT: IM_NONE;

  /* reads the bpp per plane */
  binFileRead1ByteBuf(file_bin, &bppp, 1);

  /* reads the image width and height */
  binFileRead2BytesBuf(file_bin, &xmin, 1);
  binFileRead2BytesBuf(file_bin, &ymin, 1);
  binFileRead2BytesBuf(file_bin, &xmax, 1);
  binFileRead2BytesBuf(file_bin, &ymax, 1);
  ifile->width = xmax - xmin + 1;
  ifile->height = ymax - ymin + 1;

  if (imResolutionCB)
  {
    double xres = 0, yres = 0;
    int res_unit = IM_RES_DPI;

    /* read the x resolution */
    binFileRead2BytesBuf(file_bin, &word, 1);
    xres = word;

    /* read the y resolution */
    binFileRead2BytesBuf(file_bin, &word, 1);
    yres = word;

    imResolutionCB(filename, &xres, &yres, &res_unit);
  }
  else
  {
    /* jump 4 bytes */
    binFileSeekOffset(file_bin, 4);
  }

  /* jump 3*16+1 bytes */
  binFileSeekOffset(file_bin, 3*16+1);

  /* reads the number of planes */
  binFileRead1ByteBuf(file_bin, &planes, 1);
  file_pcx->bpp = bppp * planes;

  /* reads the number of bytes per line per plane */
  binFileRead2BytesBuf(file_bin, &file_pcx->bplp, 1);

  if (binFileError(file_bin))
  {
    binFileClose(file_bin);
    free(file_pcx);
    free(ifile);
    if (err) *err = IM_ERR_READ;
    return NULL;
  }

  if (file_pcx->bpp > 8)
    ifile->type = IM_RGB;
  else
    ifile->type = IM_MAP;

  switch (file_pcx->bpp)
    {
    case 1:
      ifile->pal_size = 2;
      break;
    case 4:
      ifile->pal_size = 16;
      break;
    case 8:
      ifile->pal_size = 256;
      break;
    case 24:
      ifile->pal_size = 0;
      break;
    default:
      ifile->pal_size = 0;
      file_pcx->line_buffer = NULL;
      file_pcx->rgb_line_buffer = NULL;
      if (err) *err = IM_ERR_TYPE;
      return ifile;
    }

  /* now initialize the imFile control data */

  ifile->split = 0;     /* image is RGB packed */
  ifile->planes[0] = 0; /* sets the default plane order since split = 0 */
  ifile->planes[1] = 1;
  ifile->planes[2] = 2;
  ifile->cur_line = ifile->height - 1;  /* first line */
  ifile->pre_colors = 0;  /* color map is after the image */

  /* Calculate the size of the scan line buffer in bytes */
  file_pcx->line_size = file_pcx->bplp * planes;
  file_pcx->line_buffer = (unsigned char*)malloc(file_pcx->line_size);
  file_pcx->rgb_line_buffer = NULL;

  if (err) *err = IM_ERR_NONE;

  return ifile;
}


static imFile* imPCXWriteOpen(char* filename, int width, int height, int type, int compress, int pal_size, int *err)
{
  unsigned short word_value;
  unsigned char byte_value, filler[54+3*2];
  imFile *ifile = (imFile*)malloc(sizeof(imFile));
  imFilePCX *file_pcx = (imFilePCX*)malloc(sizeof(imFilePCX));
  binFile* file_bin;

  ifile->file = file_pcx;

  /* opens the binary file for writing with intel unsigned char order */
  file_bin = binFileOpen(filename, 1, 0);
  if (file_bin == NULL)
  {
    free(file_pcx);
    free(ifile);
    if (err) *err = IM_ERR_OPEN;
    return NULL;
  }

  file_pcx->file_bin = file_bin;

  ifile->width = width;
  ifile->height = height;
  ifile->type = type;
  ifile->pal_size = (type == IM_RGB)? 0: pal_size;
  ifile->compress = compress;

  /* now initialize the imFile control data */

  ifile->split = 0;     /* image is RGB packed */
  ifile->planes[0] = 0; /* sets the default plane order since split = 0 */
  ifile->planes[1] = 1;
  ifile->planes[2] = 2;
  ifile->cur_line = ifile->height - 1;  /* first line */
  ifile->pre_colors = 0;  /* color map is after the image */

  file_pcx->version = 5;

  if (ifile->compress)
    file_pcx->encoding = (unsigned char)1;
  else
    file_pcx->encoding = (unsigned char)0;

  file_pcx->bpp = (ifile->type == IM_RGB)? 24:8;
  file_pcx->bplp = (unsigned short)(ifile->width % 2? ifile->width + 1: ifile->width);

  /* writes the PCX file header */

  byte_value = PCX_ID;
  binFileWrite1ByteBuf(file_bin, &byte_value, 1); /* identifier */
  binFileWrite1ByteBuf(file_bin, &file_pcx->version, 1); /* format version */
  binFileWrite1ByteBuf(file_bin, &file_pcx->encoding, 1); /* compression encoding */
  byte_value = 8;
  binFileWrite1ByteBuf(file_bin, &byte_value, 1); /* bits/pixel/plane */
  word_value = 0;
  binFileWrite2BytesBuf(file_bin, &word_value, 1); /* xmin */
  binFileWrite2BytesBuf(file_bin, &word_value, 1); /* ymin */
  word_value = (unsigned short)(ifile->width - 1);
  binFileWrite2BytesBuf(file_bin, &word_value, 1); /* xmax */
  word_value = (unsigned short)(ifile->height - 1);
  binFileWrite2BytesBuf(file_bin, &word_value, 1); /* ymax */
  
  if (imResolutionCB)
  {
    double xres = 0, yres = 0;
    int res_unit = IM_RES_NONE;

    imResolutionCB(filename, &xres, &yres, &res_unit);

    if (res_unit != IM_RES_NONE)
    {
      if (res_unit == IM_RES_DPC)
      {
        xres *= 2.54;
        yres *= 2.54;
      }

      /* write the x resolution */
      word_value = (unsigned short)xres;
      binFileWrite2BytesBuf(file_bin, &word_value, 1);

      /* write the y resolution */
      word_value = (unsigned short)yres;
      binFileWrite2BytesBuf(file_bin, &word_value, 1);
    }
    else
    {
      word_value = 0;
      binFileWrite2BytesBuf(file_bin, &word_value, 1); /* hdpi */
      binFileWrite2BytesBuf(file_bin, &word_value, 1); /* vdpi */
    }
  }
  else
  {
    word_value = 0;
    binFileWrite2BytesBuf(file_bin, &word_value, 1); /* hdpi */
    binFileWrite2BytesBuf(file_bin, &word_value, 1); /* vdpi */
  }

  binFileWrite1ByteBuf(file_bin, imPCXDefaultPalette, 3*16); /* 16 colors palette */
  byte_value = 0;
  binFileWrite1ByteBuf(file_bin, &byte_value, 1); /* reserved */
  byte_value = (unsigned char)((ifile->type == IM_RGB)? 3: 1);
  binFileWrite1ByteBuf(file_bin, &byte_value, 1); /* planes */
  word_value = (unsigned short)file_pcx->bplp;
  binFileWrite2BytesBuf(file_bin, &word_value, 1); /* bytes per line per plane */
  memset(filler, 0, 54+3*2);
  binFileWrite1ByteBuf(file_bin, filler, 54+3*2); /* palette info, hscreen size, vscreen size, filler */

  /* tests if everything was ok */
  if (binFileError(file_bin))
  {
    binFileClose(file_bin);
    free(file_pcx);
    free(ifile);
    if (err) *err = IM_ERR_WRITE;
    return NULL;
  }

  /* Calculate the size of the scan line buffer in unsigned chars */
  file_pcx->line_size = file_pcx->bplp * ((ifile->type == IM_RGB)? 3: 1);
  /* the comppression buffer needs to be bigger than the image line */
  file_pcx->line_buffer = (unsigned char*)malloc(file_pcx->line_size + 512);

  /* allocates the rgb compact buffer necessary to the pcx format */
  if (ifile->type == IM_RGB)
    file_pcx->rgb_line_buffer = (unsigned char*)malloc(ifile->width*3);
  else
    file_pcx->rgb_line_buffer = NULL;

  if (err) *err = IM_ERR_NONE;

  return ifile;
}


static void imPCXClose(imFile* ifile)
{
  imFilePCX *file_pcx = (imFilePCX*)ifile->file;
  binFile* file_bin = file_pcx->file_bin;

  binFileClose(file_bin);

  if (file_pcx->rgb_line_buffer) free(file_pcx->rgb_line_buffer);
  if (file_pcx->line_buffer) free(file_pcx->line_buffer);
  free(file_pcx);
  free(ifile);
}


static int imPCXReadColors(imFile* ifile, long *colors)
{
  int c, i;
  unsigned char pcx_colors[256 * 3];
  imFilePCX *file_pcx = (imFilePCX*)ifile->file;
  binFile* file_bin = file_pcx->file_bin;

	if (file_pcx->version == 5 && file_pcx->bpp == 8)
		{
		unsigned char ExtPal;

    /* jump to the end of file minus the palette data */
    binFileSeekFrom(file_bin, -769);

    /* reads palette identifier */
    binFileRead1ByteBuf(file_bin, &ExtPal, 1);

		if (ExtPal != 12)
			return IM_ERR_READ;

    /* reads palette colors */
    binFileRead1ByteBuf(file_bin, pcx_colors, 768);
		}
	else if (file_pcx->version == 3)
    memcpy(pcx_colors, imPCXDefaultPalette, ifile->pal_size * 3);
	else
		{
    /* jump to the begining of the file at the start of the palette data */
    binFileSeekTo(file_bin, 4+6*2);

    /* reads palette colors */
    binFileRead1ByteBuf(file_bin, pcx_colors, 3 * 16);
		}

  if (binFileError(file_bin))
    return IM_ERR_READ;

  /* convert the color map to the IM format */
  for (c = 0; c < ifile->pal_size; c++)
  {
    i = c * 3;                       
    colors[c] = imEncodeColor(pcx_colors[i], pcx_colors[i+1], pcx_colors[i+2]);
  }

  return IM_ERR_NONE;
}


static int imPCXWriteColors(imFile* ifile, long *colors)
{
  unsigned char ExtPal = (unsigned char)12;
  int c, i;
  unsigned char pcx_colors[256 * 3];
  imFilePCX *file_pcx = (imFilePCX*)ifile->file;
  binFile* file_bin = file_pcx->file_bin;

  /* convert the color map to the IM format */
  for (c = 0; c < ifile->pal_size; c++)
  {
    i = c * 3;
    imDecodeColor(&pcx_colors[i], &pcx_colors[i+1], &pcx_colors[i+2], colors[c]);
  }

  /* writes the palette identifier */
  binFileWrite1ByteBuf(file_bin, &ExtPal, 1);

  /* writes the color palette */
  binFileWrite1ByteBuf(file_bin, pcx_colors, 256 * 3);

  if (binFileError(file_bin))
    return IM_ERR_WRITE;

  return IM_ERR_NONE;
}


static int imPCXReadLine(imFile* ifile, unsigned char *map)
{
  int x;
  imFilePCX *file_pcx = (imFilePCX*)ifile->file;
  binFile* file_bin = file_pcx->file_bin;

  /* if is the first, line jump to the begin of image data */
  if (ifile->cur_line == ifile->height - 1)
    binFileSeekTo(file_bin, 128);

  /* read and decompress the data */
  if (file_pcx->encoding)
  {
    if (imPCXDecodeScanLine(file_bin, file_pcx->line_buffer, file_pcx->line_size) == IM_ERR_READ)
      return IM_ERR_READ;     
  }
  else
  {
    binFileRead1ByteBuf(file_bin, file_pcx->line_buffer, file_pcx->line_size);
    if (binFileError(file_bin))
      return IM_ERR_READ;     
  }

  /* convert the buffer in the PCX bpp to im 8 bpp. */
  switch (file_pcx->bpp)
  {
  case 1:
    for (x = 0; x < ifile->width; x++)
      map[x] = (unsigned char)((file_pcx->line_buffer[x / 8] >> (7 - x % 8)) & 0x01);
    break;
  case 4:
    {
      int p, P = 8, WidthDiv8 = (ifile->width + 7) / 8;
      unsigned char nibbles, r, g, b, a;

      for (x = 0; x < WidthDiv8; x++)
      {
        r = file_pcx->line_buffer[x];
        g = (file_pcx->line_buffer + file_pcx->bplp)[x];
        b = (file_pcx->line_buffer + 2 * file_pcx->bplp)[x];
        a = (file_pcx->line_buffer + 3 * file_pcx->bplp)[x];

        if (x == WidthDiv8 - 1)
          P = ifile->width % 8;

        for (p = 0; p < P; p++)
        {
          nibbles = 0;

          /* If the most significant bit is set... */
          /* Set the appropriate bit in the higher order nibble */
          if (r & '\x80') nibbles |= 0x01;
          if (g & '\x80') nibbles |= 0x02;
          if (b & '\x80') nibbles |= 0x04;
          if (a & '\x80') nibbles |= 0x08;
          r<<=1; g<<=1; b<<=1; a<<=1;

          *(map) = nibbles;
          map++;
        }
      }

      break;
    }
  case 8:
    memcpy(map, file_pcx->line_buffer, ifile->width);
    break;
  }

  /* we have just read one more line */
  ifile->cur_line--;

  return IM_ERR_NONE;
}


static int imPCXWriteLine(imFile* ifile, unsigned char *map)
{
  int line_size;
  imFilePCX *file_pcx = (imFilePCX*)ifile->file;
  binFile* file_bin = file_pcx->file_bin;

  /* compress and writes the data */
  /* the compressed buffer size will probably be diferent from the uncompressed buffer size */
  if (file_pcx->encoding)
  {
    line_size = imPCXEncodeScanLine(file_pcx->line_buffer, map, ifile->width);
  }
  else
  {
    memcpy(file_pcx->line_buffer, map, ifile->width);
    line_size = file_pcx->line_size;
  }

  binFileWrite1ByteBuf(file_bin, file_pcx->line_buffer, line_size);
  if (binFileError(file_bin))
    return IM_ERR_WRITE;     

  /* we have just write one more line */
  ifile->cur_line--;

  return IM_ERR_NONE;
}


static int imPCXReadLineRGB(imFile* ifile, unsigned char *red, unsigned char *green, unsigned char *blue)
{
  imFilePCX *file_pcx = (imFilePCX*)ifile->file;
  binFile* file_bin = file_pcx->file_bin;

  /* if is the first, line jump to the begin of image data */
  if (ifile->cur_line == ifile->height - 1)
    binFileSeekTo(file_bin, 128);

  /* read and decompress the data */
  if (file_pcx->encoding)
  {
    if (imPCXDecodeScanLine(file_bin, file_pcx->line_buffer, file_pcx->line_size) == IM_ERR_READ)
      return IM_ERR_READ;     
  }
  else
  {
    binFileRead1ByteBuf(file_bin, file_pcx->line_buffer, file_pcx->line_size);
    if (binFileError(file_bin))
      return IM_ERR_READ;     
  }

	memcpy(red, file_pcx->line_buffer, ifile->width);
	memcpy(green, file_pcx->line_buffer + file_pcx->bplp, ifile->width);
	memcpy(blue, file_pcx->line_buffer + 2 * file_pcx->bplp, ifile->width);
	
  /* we have just read one more line */
  ifile->cur_line--;

  return IM_ERR_NONE;
}


static int imPCXWriteLineRGB(imFile* ifile, unsigned char *red, unsigned char *green, unsigned char *blue)
{
  int line_size;
  imFilePCX *file_pcx = (imFilePCX*)ifile->file;
  binFile* file_bin = file_pcx->file_bin;

	memcpy(file_pcx->rgb_line_buffer, red, ifile->width);
	memcpy(file_pcx->rgb_line_buffer + file_pcx->bplp, green, ifile->width);
	memcpy(file_pcx->rgb_line_buffer + 2 * file_pcx->bplp, blue, ifile->width);

  /* compress and writes the data */
  /* the compressed buffer size will probably be diferent from the uncompressed buffer size */
  if (file_pcx->encoding)
  {
    line_size = imPCXEncodeScanLine(file_pcx->line_buffer, file_pcx->rgb_line_buffer, ifile->width * 3);
  }
  else
  {
    memcpy(file_pcx->line_buffer, file_pcx->rgb_line_buffer, ifile->width * 3);
    line_size = file_pcx->line_size;
  }

  binFileWrite1ByteBuf(file_bin, file_pcx->line_buffer, line_size);
  if (binFileError(file_bin))
    return IM_ERR_WRITE;     

  /* we have just write one more line */
  ifile->cur_line--;

  return IM_ERR_NONE;
}


static int imPCXRegisterCallback(imCallback cb, int id)
{
  (void)cb;
  (void)id;
  /* callback not found */
  return 0;
}


static imFormat _imFormatPCX =
{
  0,
  imPCXRegisterCallback,
  imPCXReadOpen,
  imPCXWriteOpen,
  imPCXClose,
  imPCXReadColors,
  imPCXWriteColors,
  imPCXReadLine,
  imPCXWriteLine,
  imPCXReadLineRGB,
  imPCXWriteLineRGB
};

imFormat* imFormatPCX(void)
{
  return &_imFormatPCX;
}
